package br.edu.unifei.mestrado.mn;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import br.edu.unifei.mestrado.commons.graph.GraphWrapper;
import br.edu.unifei.mestrado.commons.graph.NodeWrapper;
import br.edu.unifei.mestrado.commons.graph.RelationshipWrapper;
import br.edu.unifei.mestrado.commons.mn.TempRelationships;

public class Matching {

	private Logger logger = LoggerFactory.getLogger(this.getClass());

	// Node nivel mais detalhado | Id do node do nivel mais contraido
	private Map<NodeWrapper, Long> insideOf = new HashMap<NodeWrapper, Long>();

	private boolean hasInsideOf(NodeWrapper node) {
		return insideOf.containsKey(node);
	}

	private void setInsideOf(NodeWrapper node, Long coarsedNodeId) {
		insideOf.put(node, coarsedNodeId);
	}

	private Long getInsideOf(NodeWrapper node) {
		return insideOf.get(node);
	}

	public Map<NodeWrapper, Long> getInsideOf() {
		return insideOf;
	}

	public void coarseVertex(GraphWrapper newGrafo, NodeWrapper v1, NodeWrapper v2) {
		// UTIL: cria o novo vertice, com um id novo para o grafo novo. Esse id vem do startNode
		NodeWrapper coarsedVertex = newGrafo.createNode(v1.getId(), v1.getWeight() + v2.getWeight());
		// seta aqui, no fim do método, porque o addArestasFromVertex pega o insideOf se já existir, e daí
		// sempre vai existir... NÂO: sempre não. pq ele verifica o insideOf do other ;)
		// senão vai gerar uma auto aresta para o coarsedVertex
		// TODO: precisa voltar para o começo do método, pq o método addArestasFromRemainingVertex usa ele
		// por causa do hasInsideOf do no contraido

		setInsideOf(v1, coarsedVertex.getId());
		setInsideOf(v2, coarsedVertex.getId());

		// por causa dessa troca, precisa setar o coarsed no grafoOld.
		// v1.setInsideOf(coarsedVertex.getId());
		// v2.setInsideOf(coarsedVertex.getId());
		coarsedVertex.setCoarsed(true); // indica que ele eh contraido

		logger.debug("Contraindo vertices: " + v1.getId() + "-" + v2.getId() + " : Coarsed "
				+ coarsedVertex.getId() + " Peso: " + coarsedVertex.getWeight() + "\n");

		ArrayList<TempRelationships> newArestas = new ArrayList<TempRelationships>();
		addArestasFromVertex(v1, v2.getId(), coarsedVertex, newArestas);
		imprimeArestasTmp(newArestas);

		addArestasFromVertex(v2, v1.getId(), coarsedVertex, newArestas);
		imprimeArestasTmp(newArestas);

		Collection<TempRelationships> arestasHandled = handleArestasParalelas(coarsedVertex, newArestas);
		imprimeArestasTmp(arestasHandled);

		for (TempRelationships edge : arestasHandled) {
			// adiciona a aresta no grafo
			newGrafo.createChain(edge.getId(), edge.getWeight(), edge.getStartNode(), edge.getwStartNode(),
					edge.getEndNode(), edge.getwEndNode());
		}
	}

	/**
	 * Adiciona as arestas ligadas no vertice oldVertex, em coarsedVertex. <br>
	 * Só adiciona a aresta na lista newArestas se a outra ponta da aresta ligada ao vertice oldVertex, também
	 * já estiver sido contraída antes.<br>
	 * Isso ocorre porque o metodo {@link #addArestasFromRemainingVertex(NodeWrapper, NodeWrapper, ArrayList)}
	 * ira adicionar as arestas para os vertices nao contraidos
	 * 
	 * @param oldVertex
	 *            vertice do nivel N - 1
	 * @param oldVertexOtherId
	 *            id do vertice do nivel N - 1 correspondente a outra ponta de oldVertex, usado para não
	 *            incluir a aresta que está sendo contraida no próximo nivel
	 * @param coarsedVertex
	 *            vertice do nivel N
	 * @param newArestas
	 *            lista para armazenar as novas arestas contraídas
	 */
	private void addArestasFromVertex(NodeWrapper oldVertex, long oldVertexOtherId,
			NodeWrapper coarsedVertex, List<TempRelationships> newArestas) {

		// UTIL: peso de um nó que já existe no grafo. só coloquei ele para reaproveitar a função do
		// createChain
		int weightFake = 1;
		// for para adicionar as arestas de oldV
		for (RelationshipWrapper aresta : oldVertex.getRelationships()) {
			NodeWrapper otherOld = aresta.getOtherNode(oldVertex);
			// a aresta que está sendo contraida não deve ser processada, pois ela está sendo contraida em um
			// único nó
			// UTIL: melhor explicando: as arestas entre os dois nós contraídos não sao processadas.
			// UTIL: esse if evita que arestas entre os dois nós, criem auto-arestas dos vertices contraidos.
			// :)
			if (otherOld.getId() != oldVertexOtherId) {
				// UTIL: So adiciona uma nova aresta se o outro lado já estiver contraido
				// se não foi contraído, pode ser que ele seja agrupado através de outra aresta.

				// if (otherOld.hasInsideOf()) {
				if (hasInsideOf(otherOld)) {
					// se o vertice já foi contraido, então pega o vertice correspondente,
					// long otherCoarsed = otherOld.getInsideOf();
					long otherCoarsed = getInsideOf(otherOld);

					// Repassa o peso da aresta contraida para a aresta nova.
					int weight = aresta.getWeight();
					// Cria uma nova aresta com o novo nó contraído e o outro no contraido anteriormente.
					TempRelationships newEdge = new TempRelationships(aresta.getId(), weight, coarsedVertex
							.getId(), coarsedVertex.getWeight(), otherCoarsed, weightFake);

					// add aresta
					newArestas.add(newEdge);
				}
			}
		}
	}

	/**
	 * Trata as arestas paralelas se existir.
	 * 
	 * @param cVertex
	 *            vértice contraído
	 * @param newArestas
	 *            arestas novas ligadas a esse vértice
	 * @return
	 */
	private Collection<TempRelationships> handleArestasParalelas(NodeWrapper cVertex,
			ArrayList<TempRelationships> newArestas) {
		Map<Long, TempRelationships> hash = new HashMap<Long, TempRelationships>();

		for (TempRelationships are1 : newArestas) {
			// UTIL: usa o vb, pois o va é sempre o coarsedVertex
			TempRelationships are2 = hash.get(are1.getEndNode());
			if (are2 == null) {
				// cria uma aresta nova, ligando o cVertex e o endNode
				TempRelationships coarsedEdge = new TempRelationships(are1.getId(), are1.getWeight(), cVertex
						.getId(), cVertex.getWeight(), are1.getEndNode(), are1.getwEndNode());
				hash.put(are1.getEndNode(), coarsedEdge);
			} else {
				are2.sumWeight(are1.getWeight());
			}
		}
		return hash.values();
	}

	//***************************************************************************************************
	//DAQUI PARA BAIXO SÃO MÉTODOS PARA TRATAR AS ARESTAS REMANECENTES APÓS A CONTRAÇÃO DE TODOS OS NODES
	//***************************************************************************************************
	
	/*
	 * Processa os vertices que não foram emparelhados. Repassa os vertices não processados e suas respectivas
	 * arestas
	 */
	public void processRemainingVertices(Map<NodeWrapper, Boolean> nodes, GraphWrapper newGrafo) {

		int i = 0;
		int count = 0;
		for (NodeWrapper oldNode : nodes.keySet()) {
			if (!nodes.get(oldNode)) {//TODO: verificar esse if, pois parece que nunca vai entrar aqui.
				logger.debug("Repassando vertice: " + oldNode.getId());
				// cria um novo, para não usar a mesma instancia do grafo anterior
				NodeWrapper newVertice = newGrafo.createNode(oldNode.getId(), oldNode.getWeight());

				ArrayList<TempRelationships> newArestas = new ArrayList<TempRelationships>();

				// passa o node, pq ele tem as arestas. O newVertice ainda não tem arestas.
				addArestasFromRemainingVertex(oldNode, newVertice, newArestas);
				imprimeArestasTmp(newArestas);

				handleRemainingArestasParalelas(newArestas);
				imprimeArestasTmp(newArestas);

				for (TempRelationships edge : newArestas) {
					// adiciona a aresta no grafo
					newGrafo.createChain(edge.getId(), edge.getWeight(), edge.getStartNode(), edge
							.getwStartNode(), edge.getEndNode(), edge.getwEndNode());
				}
				i++;
			}
			count++;
		}
		logger.info("Qtd vertices: " + count + " qtd Coarsed repassados: " + i);
	}

	/*
	 * Adiciona as arestas dos vertices que não foram emparelhados.
	 */
	private void addArestasFromRemainingVertex(NodeWrapper oldV, NodeWrapper newVertice,
			ArrayList<TempRelationships> newArestas) {
		// UTIL: peso de um nó que já existe. só coloquei ele para reaproveitar a função do createChain
		int weightFake = 1;
		// for para adicionar as arestas de oldV
		for (RelationshipWrapper arestaTmp : oldV.getRelationships()) {
			if (arestaTmp.getId() == 19982) {
				System.out.println("addArestasFromVertex 19982");
			}
			NodeWrapper other = arestaTmp.getOtherNode(oldV);
			long otherId = other.getId();
			// if (other.hasInsideOf()) {
			if (hasInsideOf(other)) {
				// se o vertice já foi contraido, então pega o vertice correspondente
				// otherId = other.getInsideOf();
				otherId = getInsideOf(other);
			} else {
				logger.error("Verificar !!!, não pode ter 2 vértices adjacentes sem emparelhar oldV: "
						+ oldV.getId() + " other: " + other.getId());
				// TODO: Verificar se realmente deve descartar a aresta para si mesmo.
				// não para o processamento
			}
			// o newVertice sempre vai na ponta A.
			TempRelationships newEdge = new TempRelationships(arestaTmp.getId(), arestaTmp.getWeight(),
					newVertice.getId(), newVertice.getWeight(), otherId, weightFake);

			// add aresta
			newArestas.add(newEdge);
		}
	}

	/*
	 * Processa as arestas paralelas dos vertices restantes.
	 */
	private void handleRemainingArestasParalelas(ArrayList<TempRelationships> newArestas) {
		ArrayList<TempRelationships> arestasToRemove = new ArrayList<TempRelationships>();
		// loop para ver se tem 2 arestas paralelas
		for (int i = 0; i < newArestas.size(); ++i) {
			TempRelationships are1 = newArestas.get(i);
			for (int k = i + 1; k < newArestas.size(); ++k) {
				TempRelationships are2 = newArestas.get(k);

				// UTIL: usa o vb, pois o va é sempre o vertice novo
				if (are1.getEndNode() == are2.getEndNode()) {

					// se o vb for o mesmo, então a aresta e paralela
					int peso = are1.getWeight() + are2.getWeight();

					// cria uma aresta nova
					TempRelationships coarsedEdge = new TempRelationships(are1.getId(), peso, are1
							.getStartNode(), are1.getwStartNode(), are1.getEndNode(), are1.getwEndNode());

					// substitui a aresta j
					newArestas.set(i, coarsedEdge);

					// remove a aresta k (are2)
					arestasToRemove.add(are2);
				}
			}
		}
		newArestas.removeAll(arestasToRemove);
	}

	public void imprimeArestasTmp(final Iterable<TempRelationships> newArestas) {
		logger.debug("Arestas: {}", new Object() {
			@Override
			public String toString() {
				StringBuffer b = new StringBuffer();
				for (TempRelationships edge : newArestas) {
					b.append(edge + ", ");
				}
				return b.toString();
			}
		});
	}
}
